import json
from functools import wraps

from flask import request, jsonify

from app.common.log_utils import logger
from app.common.time_utils import Datetime


class Field:
    def __init__(self, name, allow_none=True, default=None, skip_none=False):
        self.name = name
        self.allow_none = allow_none
        self.default = default
        self.skip_none = skip_none

    def __call__(self, arg_name, args):
        errs = []
        data = args.get(arg_name)
        if data is None:
            if self.allow_none:
                data = self.default() if callable(self.default) else self.default
            else:
                errs.append(f'{arg_name} is none')
        return data, errs


class BoolField(Field):
    def __init__(self, name, default=False, skip_none=True):
        super().__init__(name, True, default, skip_none)

    def __call__(self, arg_name, args):
        data, errs = super().__call__(arg_name, args)
        return data == 'True' or data == 'true' or data is True, errs


class MongoField(Field):
    def __init__(self, name, allow_none=True, default=None, skip_none=True):
        super().__init__(name, allow_none, default, skip_none)
        self.mongo_name = ''

    def __call__(self, arg_name, args):
        data, errs = super().__call__(arg_name, args)
        self.mongo_name = self.name
        if isinstance(data, str):
            if data.startswith('>='):
                self.mongo_name = self.name + '__gte'
                data = self.to_float(data[2:])
            elif data.startswith('>'):
                self.mongo_name = self.name + '__gt'
                data = self.to_float(data[1:])
            elif data.startswith('<='):
                self.mongo_name = self.name + '__lte'
                data = self.to_float(data[2:])
            elif data.startswith('<'):
                self.mongo_name = self.name + '__lt'
                data = self.to_float(data[1:])
            elif data.startswith('='):
                self.mongo_name = self.name
                data = self.to_float(data[1:])
            elif data.startswith('!='):
                self.mongo_name = self.name + '__ne'
                data = self.to_float(data[2:])
            else:
                data = self.to_float(data)
        return data, errs

    @staticmethod
    def to_float(data):
        try:
            return float(data)
        except:
            return data


class IntField(Field):
    def __init__(self, name, allow_none=True, default=None, skip_none=False):
        super().__init__(name, allow_none, default, skip_none)

    def __call__(self, arg_name, args):
        data, errs = super().__call__(arg_name, args)
        if data is not None:
            try:
                data = int(data)
            except ValueError as e:
                errs.append(f'{arg_name} {e}')
        return data, errs


class FloatField(Field):
    def __init__(self, name, allow_none=True, default=None, skip_none=False):
        super().__init__(name, allow_none, default, skip_none)

    def __call__(self, arg_name, args):
        data, errs = super().__call__(arg_name, args)
        if data is not None:
            try:
                data = float(data)
            except ValueError as e:
                errs.append(f'{arg_name} {e}')
        return data, errs


class ListField(Field):
    def __init__(self, name, sep=',', allow_none=True, default=None, skip_none=False):
        super().__init__(name, allow_none, default, skip_none)
        self.sep = sep

    def __call__(self, arg_name, args):
        data, errs = super().__call__(arg_name, args)
        data = [e.strip() for e in data.split(self.sep)] if data else []
        return data, errs


class DateTimeField(Field):
    def __init__(self, name, allow_none=True, default=None, skip_none=False):
        super().__init__(name, allow_none, default, skip_none)

    def __call__(self, arg_name, args):
        data, errs = super().__call__(arg_name, args)
        if isinstance(data, str):
            data = Datetime.strptime(data)
        return data, errs


class JsonField(Field):
    def __init__(self, name, allow_none=True, default=None, schema=None, skip=False):
        super().__init__(name, allow_none, default or {}, skip_none=False)
        self.schema = schema
        self.skip = skip

    def __call__(self, arg_name, args):
        data, errs = super().__call__(arg_name, args)
        if isinstance(data, str):
            data = json.loads(data)

        if self.schema and data is not None:
            data, _errs = _parse_args(data, self.schema)
            errs.extend(_errs)
        return data, errs


class FuncField(Field):
    def __init__(self, name, allow_none=True, default=None, skip_none=False, callback=None):
        super().__init__(name, allow_none, default, skip_none)
        self.callback = callback

    def __call__(self, arg_name, args):
        assert callable(self.callback), f'{self.callback} is not callable'
        errs = []
        data = self.callback(arg_name, args, self)
        if data is None:
            if self.allow_none:
                data = self.default() if callable(self.default) else self.default
            else:
                errs.append(f'{arg_name} is none')
        return data, errs


def parse_arguments(schema, source='q', *args):
    def decorator(func):
        @wraps(func)
        def wrapped(*args, **kwargs):
            if source == 'json':
                data = request.get_json(force=True)
            elif source == 'form':
                data = request.form
            else:
                data = request.args
            parsed_args, errs = _parse_args(data, schema)
            if errs:
                logger.error(errs)
                return jsonify({'message': errs}), 400
            return func(parsed_args, *args, **kwargs)

        return wrapped

    return decorator


def _parse_args(data, schema):
    args = {}
    errs = []
    for arg_name, field in schema.items():
        _data, _errs = field(arg_name, data)
        if _data is None and field.skip_none:
            continue
        elif isinstance(field, MongoField):
            args[field.mongo_name] = _data
        elif isinstance(field, JsonField) and field.skip:
            args.update(_data)
        else:
            args[field.name] = _data
        errs.extend(_errs)
    return args, errs
